Skip to content

S10-01 TypeScript-基础

[TOC]

TS 介绍

JS 类型缺失

JS 一门优秀的语言

我始终相信:任何新技术的出现都是为了解决原有技术的某个痛点。

JavaScript 是一门优秀的编程语言吗?

  • 每个人可能观点并不完全一致,但是从很多角度来看,JavaScript 是一门非常优秀的编程语言;

  • 而且,可以说在很长一段时间内这个语言不会被代替,并且会在更多的领域被大家广泛使用;

著名的 Atwood 定律:

  • Stack Overflow 的创立者之一的 Jeff Atwood 在 2007 年提出了著名的 Atwood 定律

  • any application that can be written in JavaScript, will eventually be written in JavaScript.

  • 任何可以使用 JavaScript 来实现的应用都最终都会使用 JavaScript 实现。

其实我们已经看到了,这句话正在一步步被应验:

  • Web 端的开发我们一直都是使用 JavaScript;

  • 移动端开发可以借助于 ReactNative、Weex、Uniapp 等框架实现跨平台开发;

  • 小程序端的开发也是离不开 JavaScript;

  • 桌面端应用程序我们可以借助于 Electron 来开发;

  • 服务器端开发可以借助于 Node 环境使用 JavaScript 来开发。

JavaScript 的痛点

并且随着近几年前端领域的快速发展,让 JavaScript 迅速被普及和受广大开发者的喜爱,借助于 JavaScript 本身的强大,也让使用 JavaScript 开发的人员越来越多。

优秀的 JavaScript 没有缺点吗?

  • 其实上由于各种历史因素,JavaScript 语言本身存在很多的缺点;

  • 比如 ES5 以及之前的使用的 var 关键字关于作用域的问题

  • 比如最初 JavaScript 设计的数组类型并不是连续的内存空间

  • 比如直到今天 JavaScript 也没有加入类型检测这一机制

JavaScript 正在慢慢变好

  • 不可否认的是,JavaScript 正在慢慢变得越来越好,无论是从底层设计还是应用层面。

  • ES6、7、8 等的推出,每次都会让这门语言更加现代、更加安全、更加方便。

  • 但是知道今天,JavaScript 在类型检测上依然是毫无进展(为什么类型检测如此重要,我后面会聊到)。

类型带来的问题

首先你需要知道,编程开发中我们有一个共识:错误出现的越早越好

  • 能在写代码的时候发现错误,就不要在代码编译时再发现(IDE 的优势就是在代码编写过程中帮助我们发现错误)。

  • 能在代码编译期间发现错误,就不要在代码运行期间再发现(类型检测就可以很好的帮助我们做到这一点)。

  • 能在开发阶段发现错误,就不要在测试期间发现错误,

  • 能在测试期间发现错误,就不要在上线后发现错误。

现在我们想探究的就是如何在 代码编译期间 发现代码的错误:

  • JavaScript 可以做到吗?不可以,我们来看下面这段经常可能出现的代码问题。

示例: 没有类型检测的 JS 代码

image-20230511175333162

image-20230511175337973

示例: 有类型检测的 TS 代码

1、在定义函数时,限定了参数的类型为string 类型,此时传入其他类型的变量(array, number, 不传)时会报错

ts
function getLength(str: string) {
  console.log(str.length)
}

// 正确:'abc'符合str的类型
getLength('abc')
// 错误:123, 不传参数不符合str的类型
getLength(123)
getLength()
export {}

2、限定一个联合类型string | any[] ,表示既可以是 string 类型,也可以是数组类型

ts
// 2. 限定str类型为 string|any[],表示只接收string和any[]类型的参数
function foo(str: string | any[]) {
  return str.length
}

// 正确
foo('aaa')
foo(['aaa', 2323, { name: 'Jack' }])
// 错误
foo({ name: 'Tom' })
foo()
foo(123)

3、限定一个对象类型 {length: number},表示参数是一个对象类型,并且该对象内部有length 属性,且该length属性的值是一个number类型

ts
  // 3. 限定str类型为 { length: number },表示只接收有length属性的对象
  function getLength(args: { length: number }) {
    console.log(args.length)
  }
  // 正确
  getLength('aaa')
  getLength(['aaa', 'ccc', 'bbb'])
  getLength({ length: 10 })
+  const info = { name: 'Tom', length: 20 }
+  getLength(info)

  // 错误
+  getLength({ name: "Jack", length: 30 })
  getLength()
  getLength(123)

注意:

  • 上例中,{ name: 'Tom', length: 20 } 先赋值给info 再传参到getLength(info)中时,可以通过类型检测
  • 但是,直接传递{ name: 'Tom', length: 20 }getLength()时,就不能通过类型检测

类型错误

这是我们一个非常常见的错误:

  • 这个错误很大的原因就是因为 JavaScript 没有对我们传入的参数进行任何的限制,只能等到运行期间才发现这个错误;

  • 并且当这个错误产生时,会影响后续代码的继续执行,也就是整个项目都因为一个小小的错误而深入崩溃;

当然,你可能会想:我怎么可能犯这样低级的错误呢?

  • 当我们写像我们上面这样的简单的 demo 时,这样的错误很容易避免,并且当出现错误时,也很容易检查出来;

  • 但是当我们开发一个大型项目时呢?你能保证自己一定不会出现这样的问题吗?而且如果我们是调用别人的类库,又如何知道让我们传入的到底是什么样的参数呢?

但是,如果我们可以给 JavaScript 加上很多限制,在开发中就可以很好的避免这样的问题了:

  • 比如我们的 getLength 函数中 str 是一个必传的类型,调用者没有传编译期间就会报错;

  • 比如我们要求它的必须是一个 string 类型,传入其他类型就直接报错;

  • 那么就可以知道很多的错误问题在编译期间就被发现,而不是等到运行时再去发现和修改;

类型思维的缺失

我们已经简单体会到没有类型检查带来的一些问题,JavaScript 因为从设计之初就没有考虑类型的约束问题,所以造成了前端开发人员关于类型思维的缺失

  • 前端开发人员通常不关心变量或者参数是什么类型的,如果在必须确定类型时,我们往往需要使用各种判断验证;

  • 从其他方向转到前端的人员,也会因为没有类型约束,而总是担心自己的代码不安全,不够健壮

所以我们经常会说 JavaScript不适合开发大型项目,因为当项目一旦庞大起来,这种宽松的类型约束会带来非常多的安全隐患,多人员开发它们之间也没有良好的类型契约

  • 比如当我们去实现一个核心类库时,如果没有类型约束,那么需要对别人传入的参数进行各种验证来保证我们代码的健壮性;

  • 比如我们去调用别人的函数,对方没有对函数进行任何的注释,我们只能去看里面的逻辑来理解这个函数需要传入什么参数,返回值是什么类型;

JavaScript 添加类型约束

为了弥补 JavaScript 类型约束上的缺陷,增加类型约束,很多公司推出了自己的方案:

  • 2014 年,Facebook 推出了flow来对 JavaScript 进行类型检查;

  • 同年,Microsoft 微软也推出了TypeScript1.0版本;

  • 他们都致力于为 JavaScript 提供类型检查;

而现在,无疑TypeScript 已经完全胜出

  • Vue2.x 的时候采用的就是 flow 来做类型检查;

  • Vue3.x已经全线转向 TypeScript,98.3%使用 TypeScript 进行了重构;

  • Angular在很早期就使用 TypeScript 进行了项目重构并且需要使用 TypeScript 来进行开发;

  • 而甚至 Facebook 公司一些自己的产品也在使用 TypeScript;

学习 TypeScript 不仅仅可以为我们的代码增加类型约束,而且可以培养我们前端程序员具备类型思维。

  • 如果之后想要学习其他语言,比如 Java、Dart 等也会是驾轻就熟;

邂逅 TS 开发

认识 TypeScript

虽然我们已经知道 TypeScript 是干什么的,也知道它解决了什么样的问题,但是我们还是需要全面的来认识一下 TypeScript 到底是什么?

我们来看一下 TypeScript 在 GitHub 和官方上对自己的定义:

  • GitHub 说法:TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TypeScript 官网:TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.

  • 翻译一下:TypeScript 是拥有类型的 JavaScript 超集,它可以编译成普通、干净、完整的 JavaScript 代码

怎么理解上面的话呢?

  • 我们可以将 TypeScript 理解成加强版的 JavaScript。

  • JavaScript 所拥有的特性,TypeScript 全部都是支持的,并且它紧随 ECMAScript 的标准,所以 ES6、ES7、ES8 等新语法标准,它都是支持的;

  • TypeScript 在实现新特性的同时,总是保持和 ES 标准的同步甚至是领先;

  • 并且在语言层面上,不仅仅增加了类型约束,而且包括一些语法的扩展,比如枚举类型(Enum)、*元组类型(Tuple)*等;

  • 并且 TypeScript 最终会被编译成 JavaScript 代码,所以你并不需要担心它的兼容性问题,在编译时也可以不借助于Babel这样的工具,可以使用TSC转换;

所以,我们可以把 TypeScript 理解成更加强大的 JavaScript,不仅让 JavaScript 更加安全,而且给它带来了诸多好用的好用特性;

TypeScript 的特点

官方对 TypeScript 有几段特点的描述,我觉得非常到位(虽然有些官方,了解一下),我们一起来分享一下:

始于 JavaScript,归于 JavaScript

  • TypeScript 从今天数以百万计的 JavaScript 开发者所熟悉的语法和语义开始;

  • 使用现有的 JavaScript 代码,包括流行的 JavaScript 库,并从 JavaScript 代码中调用 TypeScript 代码;

  • TypeScript 可以编译出纯净、 简洁的 JavaScript 代码,并且可以运行在任何浏览器上、Node.js 环境中和任何支持 ECMAScript 3(或更高版本)的 JavaScript 引擎中;

TypeScript 是一个强大的工具,用于构建大型项目

  • 类型允许 JavaScript 开发者在开发 JavaScript 应用程序时使用高效的开发工具和常用操作比如静态检查和代码重构;

  • 类型是可选的,类型推断让一些类型的注释使你的代码的静态验证有很大的不同。类型让你定义软件组件之间的接口和洞察现有 JavaScript 库的行为;

拥有 JavaScript 最新特性

  • TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件;

  • 这些特性为高可信应用程序开发时是可用的,但是会被编译成简洁的 ECMAScript3(或更新版本)的 JavaScript;

众多项目采用 TypeScript

正是因为有这些特性,TypeScript 目前已经在很多地方被应用:

  • Angular 源码在很早就使用 TypeScript 来进行了重写,并且开发 Angular 也需要掌握 TypeScript;

  • Vue3 源码也采用了 TypeScript 进行重写,在阅读源码时你会看到大量 TypeScript 的语法;

  • 包括目前已经变成最流行的编辑器VSCode也是使用 TypeScript 来完成的;

  • 包括在 React 中已经使用的ant-design的 UI 库,也大量使用 TypeScript 来编写;

  • 目前公司非常流行Vue3+TypeScriptReact+TypeScript的开发模式;

  • 包括小程序开发,也是支持 TypeScript 的;

前端学不懂系列

在之前 deno 的 issue 里面出现了一个问题:

image-20230511175428255

大前端的发展趋势

大前端是一群最能或者说最需要折腾的开发者:

  • 客户端开发者:从 Android 到 iOS,或者从 iOS 到 Android,到 RN,甚至现在越来越多的客户端开发者接触前端相关知识(Vue、React、Angular、小程序);

  • 前端开发者:从 jQuery 到 AngularJS,到三大框架并行:Vue、React、Angular,还有小程序,甚至现在也要接触客户端开发(比如 RN、Flutter);

  • 目前又面临着不仅仅学习 ES 的特性,还要学习 TypeScript;

  • 新框架的出现,我们又需要学习新框架的特性,比如 vue3.x、react18 等等;

  • 新的工具也是层出不穷,比如 vite(版本更新也很快);

但是每一样技术的出现都会让惊喜,因为他必然是解决了之前技术的某一个痛点的,而 TypeScript 真是解决了 JavaScript 存在的很多设计缺陷,尤其是关于类型检测的。

并且从开发者长远的角度来看,学习 TypeScript 有助于我们前端程序员培养类型思维,这种思维方式对于完成大型项目尤为重要。

TS 环境搭建

tsc

tsc 是 TypeScript 官方的命令行编译器,用来检查代码,并将其编译成 JavaScript 代码

依赖包:

  • typescript
    • 安装:npm i typescript -g

基本用法:

sh
# 使用 tsconfig.json 的配置
$ tsc

# 只编译 index.ts
$ tsc index.ts

# 编译 src 目录的所有 .ts 文件
$ tsc src/*.ts

# 指定编译配置文件
$ tsc --project tsconfig.production.json

# 只生成类型声明文件,不编译出 JS 文件
$ tsc index.js --declaration --emitDeclarationOnly

# 多个 TS 文件编译成单个 JS 文件
$ tsc app.ts util.ts --target esnext --outfile index.js

命令行参数:(常用)

  • --init:在当前目录创建一个全新的tsconfig.json文件,里面是预设的设置。
  • --noEmit:``,不生成编译产物,只进行类型检查。
  • --watch-w :``,进入观察模式,只要文件有修改,就会自动重新编译。
  • --project-p :``,指定编译配置文件,或者该文件所在的目录。
  • --target:``,指定编译出来的 JS 代码的版本,TypeScirpt 还会在编译时自动加入对应的库类型声明文件。
  • --outfile:``,所有编译产物打包成一个指定文件。

TS-编译环境

在前面我们提到过,TypeScript 最终会被编译成 JavaScript 来运行,所以我们需要搭建对应的环境:

  • 我们需要在电脑上安装 TypeScript,这样就可以通过 TypeScript 的 Compiler 将其编译成 JavaScript;

image-20230512113131890

所以,我们需要先可以先进行全局的安装:

sh
# 安装命令
npm install typescript -g

# 查看版本
tsc --version

# 编译ts文件
tsc xxx.ts

TS-运行环境

如果我们每次为了查看 TypeScript 代码的运行效果,都通过经过两个步骤的话就太繁琐了:

  • 第一步:通过 tsc 编译 TypeScript 到 JavaScript 代码;

  • 第二步:在浏览器或者 Node 环境下运行 JavaScript 代码;

是否可以简化这样的步骤呢?

  • 比如编写了 TypeScript 之后可以直接运行在浏览器上?

  • 比如编写了 TypeScript 之后,直接通过 node 的命令来执行?

上面我提到的两种方式,可以通过两个解决方案来完成:

  • 方式一:通过webpack,配置本地的 TypeScript 编译环境和开启一个本地服务,可以直接运行在浏览器上;

  • 方式二:通过ts-node库,为 TypeScript 的运行提供执行环境;

方式一:webpack 配置

  • 方式一在之前的 TypeScript 文章中我已经有写过,如果需要可以自行查看对应的文章;

https://mp.weixin.qq.com/s/wnL1l-ERjTDykWM76l4Ajw

ts-node

方式二:安装 ts-node

sh
npm install ts-node -g

另外 ts-node 需要依赖 tslib@types/node 两个包:

sh
npm install tslib @types/node -g

现在,我们可以直接通过 ts-node 来运行 TypeScript 的代码:

sh
ts-node math.ts

基础

TS 变量声明

变量声明-语法

我们已经强调过很多次,在 TypeScript 中定义变量需要指定 标识符 的类型。

语法: 声明了类型后 TypeScript 就会进行类型检测,声明的类型可以称之为类型注解(Type Annotation)

js
var/let/const 标识符: 数据类型 = 赋值;

示例: 比如我们声明一个 message,完整的写法如下:

ts
let message: string = 'Hello TypeScript'

如果我们给 message 赋值其他类型的值,那么就会报错:

image-20230511175702216

注意:

  • 这里的 string 是小写的,和 String 是有区别的

  • string 是 TypeScript 中定义的字符串类型,String 是 ECMAScript 中定义的一个类

变量声明-关键字

关键字:

  • var:用来声明一个变量
  • let:用来声明一个块级作用域中的变量
  • const:用来声明一个常量,一旦被赋值,就不能再次更改

在 TypeScript 定义变量(标识符)和 ES6 之后一致,可以使用varletconst来定义。

示例:

ts
// 变量声明-关键字
var num: number = 123
let msg: string = 'Hello'
const size: number = 20

~~注意:~~在 tslint 中并不推荐使用 var 来声明变量

image-20230511175727441

可见,在 TypeScript 中并不建议再使用 var 关键字了,主要原因和 ES6 升级后 let 和 var 的区别是一样的,var 是没有块级作用域的,会引起很多的问题,这里不再展开探讨。

变量-类型推导(推断)

在开发中,有时候为了方便起见我们并不会在声明每一个变量时都写上对应的数据类型,我们更希望可以通过 TypeScript 本身的特性帮助我们推断出对应的变量类型:

示例:

ts
  let msg = 'Hello' // 类型:let msg: string
  let msg2 = 123 // 类型: let msg2: number
  let msg3 = { name: "tom" } // 类型:let msg3: { name: string }

  const test = 'Hello' // 类型:const test: 'Hello'
  const test2 = 123 // 类型: const test2: 123
+  const test3 = { name: "tom" } // 类型:const test3: { name: string }

注意:

  • 1、使用 const 推导出来的是字面量类型,如:test: "Hello"
  • 2、使用 const 推导对象时,推导出来的是正常的对象类型

特性: 推导出类型后,不能给变量赋值其他类型

如果我们给 message 赋值 123:

image-20230511175809149

这是因为在一个变量第一次赋值时,会根据后面的赋值内容的类型,来推断出变量的类型

上面的 message 就是因为后面赋值的是一个 string 类型,所以 message 虽然没有明确的说明,但是依然是一个 string 类型;

image-20230511175818498

特性: 使用 const 定义字面量类型

下例中,使用const 定义的height 的类型是1.88,而不是number 类型,这个就叫字面量类型

image-20230512121709608

总结:

  • 使用let进行类型推导,推导出来的是通用类型(如 number, string)
  • 使用const 进行类型推导,推导出来的是字面量类型

JS 数据类型

JavaScript 和 TypeScript 的数据类型

我们经常说 TypeScript 是 JavaScript 的一个超集:

image-20230513103618470

JS 类型-number 类型

数字类型是我们开发中经常使用的类型,TypeScript 和 JavaScript 一样,不区分整数类型(int)和浮点型(double),统一为 number 类型

ts
// 1、JS类型-number
let num: number = 100
num = 10
num = 8.88

学习过 ES6 应该知道,ES6 新增了二进制和八进制的表示方法,而 TypeScript 也是支持二进制八进制十六进制的表示:

ts
let num2: number = 100 // 十进制
num2 = 0b100 // 二进制
num2 = 0o100 // 八进制
num2 = 0x100 // 十六进制

JS 类型-boolean 类型

boolean 类型只有两个取值:true 和 false,非常简单

ts
// 2、JS类型-boolean
let flag: boolean = true
flag = false
flag = 10 > 5

JS 类型-string 类型

string 类型是字符串类型,可以使用单引号或者双引号表示:

ts
// 3、JS类型-string
let str: string = 'Hello'
str = '你好'

同时也支持 ES6 的模板字符串来拼接变量和字符串:

ts
let name: string = 'Tom'
let str: string = `你好,${name}`

JS 类型-Array 类型

数组类型的定义也非常简单,有两种方式:

  • Array<string>:事实上是一种泛型的写法,我们会在后续中学习它的用法;
  • string[] :表示数组类型,并且数组中存放的是字符串类型
ts
// 4、JS类型-数组(属于object类型)
let arr: Array<string> = ['aaa', 'bbb', 'ccc']
let arr2: number[] = [123, 222, 333, 444]
// 正确
arr.push('kkk')
arr2.push(999)
// 错误
// arr2.push({})

如果添加其他类型到数组中,那么会报错:

image-20230511175959769

JS 类型-Object 类型

object 对象类型可以用于描述一个对象:

ts
// 5、JS类型-object
let obj: object = {
  name: 'Tom',
  age: 18
}

但是从 myinfo 中我们不能获取数据,也不能设置数据:

js
// 错误:使用object作为类型注解,无法"设置"对象
obj.name = 'Jack' // 类型“object”上不存在属性“name”

// 错误:使用object作为类型注解,无法"获取"对象
console.log(obj.name) // 类型“object”上不存在属性“name”

image-20230511180035109

Object 类型的定义方法:

  • 1、类型推导

    ts
    // object定义方法1:类型推导
    let obj2 = {
      name: 'Jack',
      age: 22
    }

    此处 obj2 的类型为:

    ts
    let obj2: {
      name: string
      age: number
    }
  • 2、通过 type 定义类型

    ts
    // object定义方法2:通过type定义类型
    type TObj3 = {
      height: number
      gender: string
    }
    let obj3: TObj3 = {
      height: 1.88,
      gender: 'male'
    }

JS 类型-Symbol 类型

在 ES5 中,如果我们是不可以在对象中添加相同的属性名称的,比如下面的做法:

image-20230511180051395

通常我们的做法是定义两个不同的属性名字:比如 identity1 和 identity2。

但是我们也可以通过 symbol 来定义相同的名称,因为 Symbol 函数返回的是不同的值:

ts
// 6、JS类型-symbol
let s1: symbol = Symbol('name')
let s2: symbol = Symbol('name')
let p = {
  [s1]: 'Jack',
  [s2]: 'Tom'
}

p 的类型是:

ts
let p = {
  [x: symbol]: string
}

JS 类型-null 和 undefined 类型

在 JavaScript 中,undefined 和 null 是两个基本数据类型。

在 TypeScript 中,它们各自的类型也是 undefined 和 null,也就意味着它们既是实际的值,也是自己的类型:

ts
// 7、JS类型-null
let n: null = null

// 8、JS类型-undefined
let u: undefined = undefined

JS 类型-函数参数类型

函数-参数类型

函数是 JavaScript 非常重要的组成部分,TypeScript 允许我们指定函数的参数和返回值的类型

参数的类型注解: 声明函数时,可以在每个参数后添加类型注解,以声明函数接受的参数类型:

~~注意:~~函数的参数类型无法通过类型推导得出,所以在定义 ts 的函数时,需要明确指定参数的类型

ts
// 1、函数类型-参数类型
function foo(str: string) {
  console.log(str.length)
}

// 正确
foo('hello')

// 错误:传入的参数和string类型不匹配
foo()
foo(123)
foo(undefined)
foo(null)
函数-参数类型:匿名函数

匿名函数与函数声明会有一些不同:

  • 当一个函数出现在 TypeScript 可以确定该函数会被如何调用的地方时;

  • 该函数的参数会自动指定类型;

image-20230511180149238

我们并没有指定 item 的类型,但是 item 是一个 string 类型:

  • 这是因为 TypeScript 会根据 forEach 函数的类型以及数组的类型推断出 item 的类型;

  • 这个过程称之为上下文类型(contextual typing),因为函数执行的上下文可以帮助确定参数和返回值的类型;

总结: 大多数情况下,在定义一个匿名函数作为参数时,不需要为其参数添加类型注解,TS 会根据上下文自动推导出匿名函数的参数类型

函数-参数类型:对象类型

如果我们希望限定一个函数接受的参数是一个对象,这个时候要如何限定呢?

示例: 使用对象类型

ts
// 5、函数类型-参数类型:对象类型
// point: { x: number, y: number }
;+function getCoordinate(point: { x: number; y: number }) {
  console.log('x: ', point.x)
  console.log('y: ', point.y)
}
getCoordinate({ x: 100, y: 200 })

说明: 在这里我们使用了一个对象来作为类型:

  • 在对象我们可以添加属性,并且告知 TypeScript 该属性需要是什么类型;

  • 属性之间可以使用 , 或者 ; 来分割,最后一个分隔符是可选的;

  • 每个属性的类型部分也是可选的,如果不指定,那么就是 any 类型;

函数-参数类型:可选类型

对象类型也可以指定哪些属性是可选的,可以在属性的后面添加一个?

ts
  // 6、函数类型-参数类型:可选类型
+  function getCoordinate2(point: { x: number, y: number, z?: number }) { // z?: number
    console.log('x: ', point.x)
    console.log('y: ', point.y)
+    if(point.z) console.log('z: ', point.z) // 判断z存在后,才能对z进行操作
  }
  getCoordinate2({ x: 100, y: 200 })
  getCoordinate2({ x: 100, y: 200, z: 300 })

JS 类型-函数返回值类型

我们也可以添加返回值的类型注解,这个注解出现在函数列表的后面:

ts
// 2、函数类型-返回值类型
;+function sum(num1: number, num2: number): number {
  return num1 + num2
}
sum(10, 20)

注意:

  • 和变量的类型注解一样,我们通常情况下不需要返回类型注解,因为 TypeScript 会根据 return 返回值推断函数的返回类型

  • 某些第三方库处于方便理解,会明确指定返回类型,看个人喜好;

总结: 返回值的类型可以明确指定,也可以自动进行类型推导

示例: 函数的类型练习

ts
  // 3、函数类型的练习
  type TLyric = {
    time: number
    text: string
  }
+  function parseLyric(lyric: string): TLyric[] {
+    const lyrics: TLyric[] = []
    lyric.split(',').forEach(item => {
+      lyrics.push({ time: Date.now(), text: item })
    })
    return lyrics
  }

  const res = parseLyric('雨一直下,气氛有点融洽')
  console.log(res)

打印结果:

ts
;[
  { time: 1684317155676, text: '雨一直下' },
  { time: 1684317155676, text: '气氛有点融洽' }
]

TS 数据类型

TS 类型 - any 类型

在某些情况下,我们确实无法确定一个变量的类型,并且可能它会发生一些变化,这个时候我们可以使用 any 类型(类似于 Dart 语言中的 dynamic 类型)。

any 类型有点像一种讨巧的 TypeScript 手段:

  • 我们可以对 any 类型的变量进行任何的操作,包括获取不存在的属性、方法;

  • 我们给一个 any 类型的变量赋任何的值,比如数字、字符串的值;

image-20230511180257896

any 的使用场景:

  • 如果对于某些情况的处理过于繁琐不希望添加规定的类型注解,

  • 在引入一些第三方库时,缺失了类型注解,这个时候我们可以使用 any:

  • 包括在 Vue 源码中,也会使用到 any 来进行某些类型的适配;

TS 类型 - unknown 类型

unknown 是 TypeScript 中比较特殊的一种类型,它用于描述类型不确定的变量

和 any 类型有点类似,但是直接在 unknown 类型的值上做任何事情都是不合法的,需要进行类型检查类型转换来保证操作的安全性;

什么意思呢?我们来看下面的场景:

ts
  // 2、TS类型-unknown
  let flag = false
+  let res: unknown // 定义res为unknown类型

  if(flag) {
    res = 'string'
  }else {
    res = 123
  }

+  if(typeof res === 'string') { // 类型缩小
    console.log(res.length)
+  }else if(typeof res === 'number') { // 类型缩小
    console.log(res + 10)
  }

  // 错误:不能直接对unknown类型进行操作
  // console.log(res.length)

TS 类型 - void 类型

void 通常用来指定一个函数是没有返回值的,那么它的返回值就是 void 类型:

ts
// 3、TS类型-void
;+function sum(num1: number, num2: number) {
  console.log(num1 + num2)
}

这个函数我们没有写任何类型,那么它默认返回值的类型就是 void的,我们也可以显式的来指定返回值是 void:

ts
// 3、TS类型-void
;+function sum(num1: number, num2: number): void {
  // : void
  console.log(num1 + num2)
}

注意: 我们可以将 undefined 赋值给 void 类型,也就是显式指定返回类型为 void 时,函数可以返回 undefined

ts
+ function sum2(num1: number, num2: number): void { // : void
  console.log(num1 + num2);
+  return undefined
}

注意:基于上下文的类型推导(Contextual Typing)推导出返回类型为 void 的时候,并不会强制函数一定不能返回内容。

image-20230513123931722

ts
const arr = ['aaa', 'bbb', 'ccc']
const res = arr.forEach(item => {
  console.log(item)
+  return item
})
+ console.log(res) // undefined

说明:forEach 函数中虽然表示返回的是 void 类型,但是在内部写上return item 也不会报错,只不过虽然 return 了,但是并没有实际效果,res 依然是 undefined

错误的做法:正常声明的函数,如果显式指定返回值为void,则不可以在内部写上return

image-20230513124120926

应用场景: 通常用于定义函数类型,指定函数类型的返回值是 void

ts
type FnType = () => void

image-20230511180353566

image-20230511180400324

~~示例:~~定义一个函数类型

image-20230513123359514

TS 类型 - never 类型

never 表示永远不会发生值的类型,比如一个函数:

  • 如果一个函数中是一个死循环或者抛出一个异常,那么这个函数会返回东西吗?

  • 不会,那么写 void 类型或者其他类型作为返回值类型都不合适,我们就可以使用 never 类型;

never 有什么样的应用场景呢?这里我们举一个例子,但是它用到了联合类型,后面我们会讲到:

应用场景:

1、开发中很少实际去定义 never 类型,某些情况下会自动推导出 never

示例: 函数抛出异常,返回 never 类型

image-20230513214507133

示例: 函数是一个死循环,返回 never 类型

image-20230513214538457

示例: 函数返回一个空数组,返回一个 never[]类型

image-20230513214913398

2、开发框架或封装工具时可能会用到 never

由于 message 只能有 number 和 string2 种类型,所以永远不能到达 default 分支,此时 check 就是 never 类型

image-20230513221145020

该工具中 never 的用处:

当后续另外添加一个 boolean 类型,忘记处理 boolean 的 case 分支时,就会在 never 分支中报错

image-20230513221510933

3、封装一些类型工具时,可以使用 never,如类型体操题目中使用 never

总结: 实际开发中只有进行类型推导时,可能会自动推导出 never 类型,但是很少使用到它

TS 类型 - tuple 类型

tuple 是元组类型,很多语言中也有这种数据类型,比如 Python、Swift 等。

image-20230511180438584

tuple 和数组的区别

  • 首先,数组中通常建议存放相同类型的元素,不同类型的元素是不推荐放在数组中。(可以放在对象或者元组中)

  • 其次,元组中每个元素都有自己特定的类型,根据索引值获取到的值可以确定对应的类型;

image-20230511180445724

应用场景: tuple 通常可以作为函数返回的值,在使用的时候会非常的方便

image-20230516153844748

语法细节

联合类型和交叉类型

联合类型 |

TypeScript 的类型系统允许我们使用多种运算符,从现有类型中构建新类型。

我们来使用第一种组合类型的方法:联合类型(Union Type)

  • 联合类型是由两个或者多个其他类型组成的类型

  • 表示可以是这些类型中的任何一个值;

  • 联合类型中的每一个类型被称之为联合成员(union's members);

image-20230516160027453

使用时注意: 不能直接对联合类型的变量直接进行操作,需要进行类型缩小

image-20230516155847239

使用联合类型

传入给一个联合类型的值是非常简单的:只要保证是联合类型中的某一个类型的值即可

  • 但是我们拿到这个值之后,我们应该如何使用它呢?因为它可能是任何一种类型。

  • 比如我们拿到的值可能是 string 或者 number,我们就不能对其调用 string 上的一些方法;

那么我们怎么处理这样的问题呢?

  • 我们需要使用缩小(narrow)联合(后续我们还会专门讲解缩小相关的功能);

  • TypeScript 可以根据我们缩小的代码结构,推断出更加具体的类型

image-20230512101606684

type 和 interface 使用

类型别名 type

在前面,我们通过在类型注解中编写 对象类型 和 联合类型,但是当我们想要多次在其他地方使用时,就要编写多次。

比如我们可以给对象类型起一个别名:

image-20230512101621894

image-20230512101615584

接口声明 interface

在前面我们通过 type 可以用来声明一个对象类型:

image-20230512101632755

对象的另外一种声明方式就是通过接口来声明:

image-20230512101640028

~~区别:~~interface 和 type

  • 类型别名和接口非常相似,在定义对象类型时,大部分时候,你可以任意选择使用。

  • 接口的几乎所有特性都可以在 type 中使用(后续我们还会学习 interface 的很多特性);

interface 和 type 区别

  • 1、type 类型可以声明非对象类型;interface 只能声明对象类型

    ts
    type Id = number | string // 正确
    interface Id = number | string // 错误
  • 2、在声明对象时,interface 可以多次声明;type 只能声明一次

    ts
    // 错误的写法:PointType声明了2次,但type只能声明1次
    type PointType = {
      x: number
      y: number
    }
    type PointType = {
      z?: number
    }
    
    // 正确的写法:interface可以多次声明
    interface PointType2 {
      x: number
      y: number
    }
    interface PointType2 {
      z?: number
    }
  • 3、interface 支持继承

    ts
    // 说明:IMan继承了IPerson的 name 和 age 属性
    interface IPerson {
      name: string
      age: number
    }
    interface IMan extends IPerson {
      gender: string
    }
  • 4、interface 可以被类实现

    ts
    class Person implements IPerson {
        ...
    }

总结:

  • 如果是非对象类型的声明使用 type
  • 如果是对象类型的声明使用 interface

交叉类型 &

前面我们学习了联合类型:

  • 联合类型表示多个类型中一个即可

image-20230512101724143

还有另外一种类型合并,就是交叉类型(Intersection Types)

  • 交叉类似表示需要满足多个类型的条件

  • 交叉类型使用 & 符号;

我们来看下面的交叉类型:

  • 表达的含义是 number 和 string 要同时满足;

  • 但是有同时满足是一个 number 又是一个 string 的值吗?其实是没有的,所以 MyType 其实是一个 never 类型;

image-20230512101730392

交叉类型的应用

1、交叉类型的主要用途是表示对象的合成。

image-20230512101742991

2、交叉类型常常用来为对象类型添加新属性。

typescript
type A = { foo: number }
type B = A & { bar: number }

上面示例中,类型B是一个交叉类型,用来在A的基础上增加了属性bar

类型断言和非空断言

类型断言 as

有时候 TypeScript 无法获取具体的类型信息,这个我们需要使用类型断言(Type Assertions)

  • 比如我们通过 document.getElementById,TypeScript 只知道该函数会返回 HTMLElement ,但并不知道它具体的类型:

image-20230512101752970

类型断言的规则:

▸ 实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。

image-20230516172454102

上面示例中,是错误的写法

▸ 如果要断言成一个完全无关的类型,可以通过连续进行两次类型断言实现。先断言成 unknown 类型或 any 类型,然后再断言为目标类型。

ts
const age = age as any as string
// 或者
const age = age as unkown as string

上面示例中,TS 检测是正确的,但代码本身不正确

非空断言 !

当我们编写下面的代码时,在执行 ts 的编译阶段会报错:

image-20230512101843286

这是因为传入的 message 有可能是为 undefined 的,这个时候是不能执行方法的。

▸ 对于那些可能为空的变量(即可能等于undefinednull),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!。它表示可以确定某个标识符是有值的,跳过 ts 在编译阶段对它的检测

image-20230512101853426

▸ 非空断言在实际编程中可以省去一些额外的判断。

ts
const root = document.getElementById('root')

root!.addEventListener('click', (e) => {
  /* ... */
})

▸ 非空断言会造成安全隐患,只有在确定一个表达式的值不为空时才能使用。比较保险的做法还是手动检查一下是否为空。

ts
const root = document.getElementById('root');

+ if (root === null) {
+   throw new Error('Unable to find DOM element #root');
+ }

root.addEventListener('click', e => {
  /* ... */
});

▸ 非空断言还可以用于赋值断言。TypeScript 有一个编译设置,要求类的属性必须初始化(即有初始值),如果不对属性赋值就会报错。

ts
class Point {
  x!: number // 正确
  y!: number // 正确

  constructor(x: number, y: number) {
    // ...
  }
}

▸ 非空断言只有在打开编译选项strictNullChecks时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为undefinednull

示例

image-20230516173902952

字面量类型和类型缩小

字面量类型

除了前面我们所讲过的类型之外,也可以使用字面量类型(literal types)

▸ TS 规定,单个值也是一种类型,称为“字面量类型(literal types)”。

image-20230512101902867

▸ TS 推断类型时,遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是字面量类型。

ts
// x 的类型是 "https"
const x = 'https'

// y 的类型是 string
const y: string = 'https'

const命令声明的变量,如果赋值为对象,并不会推断为字面量类型。

ts
// x 的类型是 { foo: number }
const x = { foo: 1 }

▸ 默认情况下这么做是没有太大的意义的,但是我们可以将多个字面量结合,作为联合类型使用。

image-20230512101912220

示例: 封装请求方法

image-20230516174723813

字面量推理

as const应用。我们来看下面的代码:

image-20230512101920078

这是因为我们的对象在进行字面量推理的时候,info 其实是一个 {url: string, method: string},所以我们没办法将一个 string 赋值给一个 字面量 类型。

解决方案 1:对 info.method 进行类型断言:info.method as 'GET'

image-20230512101927630

解决方案 2:直接对 info 进行类型断言:info {...} as const

image-20240316120357637

或者

image-20230516175501819

类型缩小

什么是类型缩小呢?

  • 类型缩小的英文是 Type Narrowing(也有人翻译成类型收窄);

  • 我们可以通过类似于 typeof padding === "number" 的判断语句,来改变 TypeScript 的执行路径

  • 在给定的执行路径中,我们可以缩小比声明时更小的类型,这个过程称之为 缩小( Narrowing );

  • 而我们编写的 typeof padding === "number 可以称之为 类型保护(type guards)

常见的类型保护有如下几种:

  • typeof

  • 平等缩小(比如===、!==)

  • instanceof

  • in

  • 等等...

类型保护 typeof

在 TypeScript 中,检查返回的值 typeof 是一种类型保护。因为 TypeScript 对如何 typeof 操作不同的值进行编码。

▸ 通过typeof进行类型缩小

image-20230512101950420

typeof 补充:

▸ TS 将typeof运算符移植到了类型运算,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TS 类型。

ts
const a = { x: 0 }

type T0 = typeof a // { x: number }
type T1 = typeof a.x // number

typeof运算符只可能返回八种结果,而且都是字符串。没有 array, null,它们都是 object 类型

ts
typeof undefined // "undefined"
typeof true // "boolean"
typeof 1337 // "number"
typeof 'foo' // "string"
typeof parseInt // "function"
typeof Symbol() // "symbol"
typeof 127n // "bigint"

typeof {} // "object"

▸ 这种用法的typeof返回的是 TS 类型,所以只能用在类型运算之中(即跟类型相关的代码之中),不能用在值运算。

ts
let a = 1;
+ let b:typeof a; // 类型运算

+ if (typeof a === 'number') { // 值运算
  b = a;
}

typeof 的参数只能是标识符,不能是需要运算的表达式。这是由于编译时不会进行 JavaScript 的值运算

ts
type T = typeof Date(); // 报错:Date()需要运算

typeof命令的参数不能是类型。

ts
type Age = number
type MyAge = typeof Age // 报错
类型保护-平等缩小

我们可以使用 switch 或者相等的一些运算符来表达相等性(比如*=*, _!_, ==, and != ):

image-20230512101958750

类型保护 instanceof

JavaScript 有一个运算符来检查一个值是否是另一个值的“实例”:

image-20230512102006507

类型保护 in

in:用于确定对象或其原型链中是否具有某个属性

image-20230517095738305

类型检测 satisfies @4.9

satisfies 运算符用来检测某个值是否符合指定类型。有时候,不方便将某个值指定为某种类型,但是希望这个值符合类型条件,这时候就可以用satisfies运算符对其进行检测。

问题: palette.green属性调用substring()方法会报错,原因是这个方法只有字符串才有,而palette.green的类型是srting|RGB,除了字符串,还可能是元组RGB,而元组并不存在substring()方法,所以报错了。

ts
type Colors = 'red' | 'green' | 'blue'
type RGB = [number, number, number]

const palette: Record<Colors, string | RGB> = {
  red: [255, 0, 0],
  green: '#00ff00',
  blue: [0, 0, 255]
}

const greenComponent = palette.green.substring(1, 6) // 报错

解决: 可以使用satisfies运算符,对palette进行类型检测,但是不改变 TypeScript 对palette的类型推断

ts
type Colors = 'red' | 'green' | 'blue'
type RGB = [number, number, number]

const palette = {
  red: [255, 0, 0],
  green: '#00ff00',
  blue: [0, 0, 255] // 报错
} satisfies Record<Colors, string | RGB>

const greenComponent = palette.green.substring(1) // 不报错

函数类型

函数类型和函数签名

函数类型

在 JavaScript 开发中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数,也可以作为返回值进行传递)。

那么在使用函数的过程中,函数是否也可以有自己的类型呢?

函数类型表达式

我们可以编写函数类型表达式(Function Type Expressions),来表示函数类型

语法

ts
// 语法一:函数类型表达式
type FunctionType = (parameter1: type, parameter2: type) => returnType

// 语法二:函数调用签名
type FunctionType = (parameter1: type, parameter2: type): returnType

// 语法三:函数构造签名
type FunctionType = new (parameter1: type, parameter2: type): returnType

在上面的语法中 (num1: number, num2: number) => void,代表的就是一个函数类型

  • 接收两个参数的函数:num1 和 num2,并且都是 number 类型;

  • 并且这个函数是没有返回值的,所以是 void;

注意:在某些语言中,可能参数名称 num1 和 num2 是可以省略,但是 TypeScript 是不可以的:

image-20230512102041522

示例

image-20230512102029336

调用匿名函数作为参数:此时不需要在匿名函数的参数上添加函数注解,可以类型推导出来

image-20230517103539852

问题:函数类型参数的个数检测问题

image-20230517103247264

*解释:*考虑到实际使用中,我们并不会传入所有的参数。TS 对于传入的函数类型的参数个数不进行检测,TS 内部多余的参数会被忽略掉,所以实际使用中我们只能少传不能多传

image-20240316134139720

TS 内部是通过判断 foo 是否继承 CalcType 来检测是否合法的

image-20240316134604570

TS 检测规则:在 TS 更好用和类型检测之间寻求平衡

image-20230517104623607

调用签名

函数除了作为一个 function 外,还可以被看做一个 object,从对象的角度看,函数除了可以被调用,还可以拥有自己的属性

在 JavaScript 中,函数除了可以被调用,自己也是可以有属性值的

  • 然而前面讲到的函数类型表达式并不能支持声明属性;

  • 如果我们想描述一个带有属性的函数,我们可以在一个对象类型中写一个调用签名(call signature);

image-20230517110253835

注意: 这个语法跟函数类型表达式稍有不同,在参数列表和返回的类型之间用的是 : 而不是 =>

开发中如何选择:

  • 如果只是描述函数类型本身(函数可以被调用),使用函数类型表达式
  • 如果在描述函数作为对象可以被调用,同时也有其他属性时,使用函数调用签名
构造签名

JavaScript 函数也可以使用 new 操作符调用,当被调用的时候,TypeScript 会认为这是一个构造函数(constructors),因为他们会产生一个新对象。

你可以写一个构造签名( Construct Signatures ),方法是在调用签名前面加一个 new 关键词

注意: 箭头函数不能通过 new 来调用,因为箭头函数没有原型(prototype)

image-20230517113418933

参数

可选参数类型

我们可以指定某个参数是可选的:

image-20240316140619997

可选参数的类型

这个时候这个参数 y 依然是有类型的,它是什么类型呢? number | undefined

image-20230512102131005

另外可选类型需要在必传参数的后面

image-20230512102138613

默认参数

从 ES6 开始,JavaScript 是支持默认参数的,TypeScript 也是支持默认参数的:

image-20230512102148628

这个时候 y 的类型其实是 undefined 和 number 类型的联合。

注意:

  • 1、有默认值的情况下,参数的类型注解可以省略
  • 2、有默认值的参数,可以接收一个 undefinded 的值
剩余参数

从 ES6 开始,JavaScript 也支持剩余参数,剩余参数语法允许我们将一个不定数量的参数放到一个数组中

image-20230512102158175

函数的重载

函数的重载(了解)

在 TypeScript 中,如果我们编写了一个 add 函数,希望可以对字符串和数字类型进行相加,应该如何编写呢?

image-20230517115135339

我们可能会这样来编写,但是其实是错误的:

image-20230512102209262

那么这个代码应该如何去编写呢?

  • 在 TypeScript 中,我们可以去编写不同的*重载签名(overload signature)*来表示函数可以以不同的方式进行调用;

  • 一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现

函数的重载

比如我们对 sum 函数进行重构:

▸ 调用 sum 的时候,会根据我们传入的参数类型来决定执行函数体时,到底执行哪一个函数的重载签名;

image-20240316152305905

▸ *注意:*有实现体的函数,是不能直接被调用的。

image-20230512102228810

联合类型和重载对比

我们现在有一个需求:定义一个函数,可以传入字符串或者数组,获取它们的长度。

这里有两种实现方案:

  • 方案一:使用联合类型来实现;

    image-20230512102239406

  • 方案二:实现函数重载来实现;

    image-20230512102251541

在开发中我们选择使用哪一种呢?

  • 在可能的情况下,尽量选择使用联合类型来实现

this 类型

this 默认类型

this 是 JavaScript 中一个比较难以理解和把握的知识点:

我在公众号也有一篇文章专门讲解 this:https://mp.weixin.qq.com/s/hYm0JgBI25grNG_2sCRlTA

当然在目前的 Vue3 和 React 开发中你不一定会使用到 this:

Vue3 的 Composition API 中很少见到 this,React 的 Hooks 开发中也很少见到 this 了;

但是我们还是简单掌握一些 TypeScript 中的 this,TypeScript 是如何处理 this 呢?我们先来看两个例子:

▸ 在没有对 TS 进行特殊配置的情况下,this 默认是 any 类型

image-20230512102304178

上面的代码默认情况下是可以正常运行的,也就是 TypeScript 在编译时,认为我们的 this 是可以正确去使用的。

这是因为在没有指定 this 的情况,this 默认情况下是 any 类型的;

this 编译选项

VSCode 在检测我们的 TypeScript 代码时,默认情况下运行不确定的 this 按照 any 类型去使用。

但是我们可以创建一个 tsconfig.json 文件,并且在其中告知 VSCode this 必须明确执行(不能是隐式的);

1、生成初始化 TS 配置文件 tsconfig.json

sh
tsc --init

2、设置tsconfig.json中的CompilerOptions.noImplicitThis: true,表示不允许 this 存在隐式的 any 类型

image-20230512102345632

在设置了 noImplicitThis 为 true 时, TypeScript 会根据上下文推导 this,但是在不能正确推导时,就会报错,需要我们明确的指定 this。

image-20230512102358298

指定 this 类型

在开启 noImplicitThis 的情况下,我们必须指定 this 的类型。

如何指定呢?函数的第一个参数类型:

  • 函数的第一个参数我们可以根据该函数之后被调用的情况,用于声明 this 的类型(名词必须叫 this);

  • 在后续调用函数传入参数时从第二个参数开始传递的,this 参数会在编译后被抹除;

image-20230512102411809

this 内置工具

Typescript 提供了一些工具类型来辅助进行常见的类型转换,这些类型全局可用。

ThisParameterType

ThisParameterType<FnType>:用于提取一个函数类型 Type 的 this (opens new window)参数类型

如果这个函数类型没有 this 参数返回 unknown;

image-20240316160518210

image-20240316160808637

OmitThisParameter

OmitThisParameter<FnType>:用于移除一个函数类型 Type 的 this 参数类型, 并且返回当前的函数类型

image-20240316160732762

image-20240316160754905

ThisType

ThisType<Type>:用于在对象字面量中指定该对象的上下文类型,不返回一个转换过的类型

image-20230517144903836

上面示例中,是将IState作为 store 中 this 的类型绑定过去的(pinia 中就是如此操作的)

面向对象

TS 类-基本使用

认识类的使用

类和继承: 在早期的 JavaScript 开发中(ES5)我们需要通过函数和原型链来实现类和继承,从ES6开始,引入了class关键字,可以更加方便的定义和使用类。

TypeScript作为 JavaScript 的超集,也是支持使用class关键字的,并且还可以对类的属性和方法等进行静态类型检测

实际上在 JavaScript 的开发过程中,我们更加习惯于函数式编程:

  • 比如 React 开发中,目前更多使用的函数组件以及结合 Hook 的开发模式;

  • 比如在 Vue3 开发中,目前也更加推崇使用 Composition API;

但是在封装某些业务的时候,类具有更强大封装性,所以我们也需要掌握它们。

类的定义我们通常会使用 class 关键字:

  • 在面向对象的世界里,任何事物都可以使用类的结构来描述;

  • 类中包含特有的属性和方法;

类的定义 class

我们来定义一个 Person 类:

  • 使用 class 关键字来定义一个类;

1、我们可以声明类的属性:在类的内部声明类的属性以及对应的类型

  • 如果类型没有声明,那么它们默认是 any 的;

  • 我们也可以给属性设置初始化值;

  • 在默认的strictPropertyInitialization 模式下面我们的属性是必须初始化的,如果没有初始化,那么编译时就会报错;

    • 如果我们在 strictPropertyInitialization 模式下确实不希望给属性初始化,可以使用 name!: string语法

2、类可以有自己的构造函数constructor,当我们通过 new 关键字创建一个实例时,构造函数会被调用;

  • 构造函数不需要返回任何值,默认返回当前创建出来的实例;

3、类中可以有自己的函数,定义的函数称之为方法;

image-20230517145846871

总结: 类的组成:属性,构造方法,方法

类的继承 extends

面向对象的其中一大特性就是继承,继承不仅仅可以减少我们的代码量,也是多态的使用前提。

我们使用extends关键字来实现继承,子类中使用super来访问父类。

我们来看一下 Student 类继承自 Person:

  • Student 类可以有自己的属性和方法,并且会继承 Person 的属性和方法;

  • 在构造函数中,我们可以通过 super 来调用父类的构造方法,对父类中的属性进行初始化

image-20230517145858323

image-20230517145904713

类的成员修饰符

在 TypeScript 中,类的属性和方法支持三种修饰符: public、private、protected

  • public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是 public 的;

  • private 修饰的是仅在同一类内部可见、私有的属性或方法;

  • protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;

public 是默认的修饰符,也是可以直接访问的,我们这里来演示一下 protected 和 private。

示例: protected

image-20230517145916264

示例: private

image-20230517145922816

只读属性 readonly

如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用readonly

image-20230517145934364

特点: 外部或内部可以访问,但是不能修改

getter/setter

在前面一些私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程,这个时候我们可以使用存取器

image-20230517145946147

image-20230517145956190

意义: 可以在 setter、getter 中对属性的访问进行拦截操作

image-20230518105359337

参数属性

TypeScript 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性

  • 这些就被称为参数属性(parameter properties)

  • 你可以通过在构造函数参数前添加一个可见性修饰符 public private protected 或者 readonly 来创建参数属性,最后这些类属性字段也会得到这些修饰符;

image-20230517150008172

TS-抽象类

抽象类 abstract

我们知道,继承是多态使用的前提

  • 所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。

    但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。

什么是 抽象方法? 在 TypeScript 中没有具体实现的方法(没有方法体),就是抽象方法。

  • 抽象方法,必须存在于抽象类中

  • 抽象类是使用abstract声明的类;

抽象类有如下的特点

  • 抽象类是不能被实例化的(也就是不能通过 new 创建)

  • 抽象类可以包含抽象方法,也可以包含有实现体的方法;

  • 有抽象方法的类,必须是一个抽象类;

  • 抽象方法必须被子类实现,否则该子类必须是一个抽象类;

抽象类

语法:

ts
abstract class 类名 {
  // 抽象方法
  abstract 方法名(参数列表): 返回值类型

  // 具体方法
  method() {
    // 代码块
  }
}

抽象方法

语法:

ts
abstract 方法名(参数列表): 返回值类型;

抽象类演练

示例: 计算不同形状的面积

image-20230517150036905

image-20230517150055538

image-20230517150045951

TS-对象类型

类的类型

类本身也是可以作为一种数据类型的:

image-20230517150106647

类的作用:

  • 1、类可以创建类对应的实例对象
  • 2、类本身可以作为这个实例的类型
  • 3、类也可以当做一个有构造签名的函数

对象类型的属性修饰符

对象类型中的每个属性可以说明它的类型、属性是否可选、属性是否只读等信息。

  • 可选属性(Optional Properties)?
    • 我们可以在属性名后面加一个 ? 标记表示这个属性是可选的;
  • 只读属性(Readonly Properties)readonly
    • 在 TypeScript 中,属性可以被标记为 readonly,这不会改变任何运行时的行为;
    • 但在类型检查的时候,一个标记为 readonly 的属性是不能被写入的。

image-20230517150116396

索引签名

索引签名: 有的时候,你不能提前知道一个类型里的所有属性的名字,但是你知道这些值的特征。这种情况,你就可以用一个索引签名 (index signature)描述可能的值的类型

image-20230518153937125

特性 1:索引签名-基本使用

image-20230517150128210

image-20230517150134987

特性 2:索引签名-类型问题

一个索引签名的属性类型必须是 stringnumbersymbol

image-20240613114954522

特性 3:索引签名-两个签名

虽然 TypeScript 可以同时支持 string 和 number 类型,但数字索引的返回类型一定要是字符索引返回类型的子类型;(了解)

两个索引类型的写法:

image-20240613115011334

要求一: 下面的写法不允许:数字索引的返回类型一定要是字符索引返回类型的子类型

原因:数字类型会转化成字符串类型去对象中获取值,如: name[0] 会转成:name['0']

image-20230518160644361

要求二: 如果索引签名中有定义其他属性,其他属性的类型必须符合 string 类型返回的值的类型

image-20230518162013650

TS-接口补充

接口继承

接口一样是可以进行继承的,也是使用extends关键字:

  • 并且我们会发现,接口是支持多继承的(类不支持多继承)

接口继承的作用:

  • 1、减少相同代码的重复编写
  • 2、使用第三方库时,可以根据对方定义的接口进行扩展

image-20230517150151839

image-20230517150200451

接口的实现

接口定义后,也是可以被类实现的:

  • 如果被一个类实现,那么在之后需要传入接口的地方,都可以将这个类传入;

  • 这就是面向接口开发;

image-20230517150212899

image-20230517150222000

接口实现的特点:

  • 1、接口中的所有属性和方法都必须被实现
  • 2、一个类或对象可以实现多个接口

抽象类和接口的区别(了解)

抽象类在很大程度上和接口会有点类似:都可以在其中定义一个方法,让子类或实现类来实现对应的方法。

那么抽象类和接口有什么区别呢?

  • 抽象类是事物的抽象,抽象类用来捕捉子类的通用特性,

    接口通常是一些行为的描述;

  • 抽象类通常用于一系列关系紧密的类之间,

    接口只是用来描述一个类应该具有什么行为;

  • 接口可以被多层实现,

    而抽象类只能单一继承;

  • 抽象类中可以有实现体,

    接口中只能有函数的声明;

通常我们会这样来描述类和抽象类、接口之间的关系:

  • 抽象类是对事物的抽象,表达的是 is a 的关系。猫是一种动物(动物就可以定义成一个抽象类)

  • 接口是对行为的抽象,表达的是 has a 的关系。猫拥有跑(可以定义一个单独的接口)、爬树(可以定义一个单独的接口)的行为。

特殊-严格字面量赋值检测

严格的字面量赋值检测

对于对象的字面量赋值,在 TypeScript 中有一个非常有意思的现象:

image-20230517150300235

image-20230517150251284

image-20230517150246096

TS 类型检测:鸭子类型

所谓的鸭式辨型法就是像鸭子一样走路并且嘎嘎叫的就叫鸭子,即具有鸭子特征的认为它就是鸭子,也就是通过制定规则来判定对象是否实现这个接口。

示例 1:

说明:没有通过new Person 来创建 Person 实例,而是直接创建了个{ name, age } 包含 name 和 age 的字面量对象,或者创建一个和 Person 没有继承关系的 Dog 实例,也可以实现类型匹配

image-20230518115417952

解释: TS 在进行类型检测时使用的是鸭子类型。鸭子类型只关心属性行为,不关心具体是不是对应的类型

示例 2:

ts
interface LabeledValue {
  label: string;
}
function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}
+ let myObj = { size: 10, label: "Size 10 Object" };
+ printLabel(myObj); // OK
ts
interface LabeledValue {
  label: string
}
function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label)
}
;+printLabel({ size: 10, label: 'Size 10 Object' }) // Error

上面代码,在参数里写对象就相当于是直接给labeledObj赋值,这个对象有严格的类型定义,所以不能多参或少参。而当你在外面将该对象用另一个变量myObj接收,myObj不会经过额外属性检查,但会根据类型推论为let myObj: { size: number; label: string } = { size: 10, label: "Size 10 Object" };,然后将这个myObj再赋值给labeledObj,此时根据类型的兼容性,两种类型对象,参照鸭式辨型法,因为都具有label属性,所以被认定为两个相同,故而可以用此法来绕开多余的类型检查。

为什么会出现这种情况呢?

这里我引入 TypeScript 成员在 GitHub 的 issue 中的回答:

image-20230517150318673

总结:

  • 第一次创建的对象字面量,称之为 fresh(新鲜的)。

    对于新鲜的字面量,会进行严格的类型检测,必须完全满足类型的要求:不能有多余的属性。

    image-20240316180139712

  • 类型断言或对象字面量的类型扩散时,新鲜度会消失。

    image-20240316180709015

TS-枚举类型

TypeScript 枚举类型

枚举类型是为数不多的 TypeScript 特性有的特性之一:

  • 枚举其实就是将一组可能出现的值,一个个列举出来,定义在一个类型中,这个类型就是枚举类型

  • 枚举允许开发者定义一组命名常量,常量可以是数字字符串类型;

image-20230517150350570

image-20230517150402500

枚举类型的值

枚举类型默认是有值的,比如上面的枚举,默认值是这样的:

image-20230518170729892

当然,我们也可以给枚举其他值。这个时候会从 100 进行递增,步长为 1;

image-20230518170744326

我们也可以给他们赋值其他的类型

image-20230518170750765

泛型

泛型语法-基本使用

认识泛型

软件工程的主要目的是构建不仅仅明确和一致的 API,还要让你的代码具有很强的可重用性:

  • 比如我们可以通过函数来封装一些 API,通过传入不同的函数参数,让函数帮助我们完成不同的操作;

  • 但是对于参数的类型是否也可以参数化呢?

什么是类型的参数化

  • 我们来提一个需求:封装一个函数,传入一个参数,并且返回这个参数;

如果我们是 TypeScript 的思维方式,要考虑这个参数和返回值的类型需要一致:

image-20230517151245288

上面的代码虽然实现了,但是不适用于其他类型,比如 string、boolean、Person 等类型:

image-20230517151251033

泛型-实现类型参数化

虽然 any 是可以的,但是定义为 any 的时候,我们其实已经丢失了类型信息:

  • 比如我们传入的是一个 number,那么我们希望返回的可不是 any 类型,而是 number 类型;

  • 所以,我们需要在函数中可以捕获到参数的类型是 number,并且同时使用它来作为返回值的类型;

我们需要在这里使用一种特性的变量 - 类型变量(type variable),它作用于类型,而不是值:

image-20230517151302021

这里我们可以使用两种方式来调用它:

  • 方式一:通过 <类型> 的方式将类型传递给函数;

    image-20230518173034800

  • 方式二:通过类型推导(type argument inference),自动推到出我们传入变量的类型:

    • 在这里会推导出它们是 字面量类型的,因为字面量类型对于我们的函数也是适用的

    image-20230518173053812

案例: 手写 useState

image-20230518174105143

泛型-传入多个类型

当然我们也可以传入多个类型:

image-20230517151341616

平时在开发中我们可能会看到一些常用的名称:

  • T:Type 的缩写,类型

  • K、V:key 和 value 的缩写,键值对

  • E:Element 的缩写,元素

  • O:Object 的缩写,对象

  • R:Return 的缩写

示例: 传入多个类型

image-20230518174645545

问题: foo<T, E>(a1: T, a2: E) 这里的 T 和 E 需要按照顺序写入么?

答: 不需要。(a1: T, a2: E)中的 T 和 E 就相当于参数的使用一样

泛型接口、类的使用

泛型接口

在定义接口的时候我们也可以使用泛型:

image-20230517151414088

为泛型接口指定默认值

image-20240607100210016

image-20240607100128224

泛型类

我们也可以编写一个泛型类:

image-20230517151432189

泛型约束、类型条件

泛型约束

有时候我们希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中:

  • 比如 string 和 array 都是有 length 的,或者某些对象也是会有 length 属性的;

  • 那么只要是拥有 length 的属性都可以作为我们的参数类型,那么应该如何操作呢?

1、getlenght()没有必要使用泛型,因为返回的类型是正确的(number)

image-20240607101109848

2、获取传入的内容,这个内容必须有 length 属性

image-20240607101731757

3、上面的写法虽然可以正确获取到返回的类型,但是对参数没有约束。可以传任何类型的参数

image-20240607101848885

4、有约束的写法。Type 相当于是一个变量,用于记录本次调用的类型,所以在整个函数的执行周期中,一直保留着参数的类型

image-20240607102025251

**说明:**这里表示是传入的类型必须有这个属性,也可以有其他属性,但是必须至少有这个成员。

泛型约束-类型参数

在泛型约束中使用类型参数(Using Type Parameters in Generic Constraints)

  • 你可以声明一个类型参数,这个类型参数被其他类型参数约束;

举个栗子:我们希望获取一个对象给定属性名的值

  • 我们需要确保我们不会获取 obj 上不存在的属性;

  • 所以我们在两个类型之间建立一个约束;

image-20230601112133061

补充: keyof IKun 返回的类型就是对象中所有 key 的联合类型

image-20240607103114785

TS-映射类型

映射类型

映射(mapping)指的是,将一种类型按照映射规则,转换成另一种类型,通常用于对象类型。

有的时候,一个类型需要基于另外一个类型,但是你又不想拷贝一份,这个时候可以考虑使用映射类型。

  • 大部分内置的工具都是通过映射类型来实现的;

  • 大多数类型体操的题目也是通过映射类型完成的;

映射类型建立在索引签名的语法上:

  • 映射类型,就是使用了 PropertyKeys 联合类型的泛型;

  • 其中 PropertyKeys 多是通过 keyof 创建,然后循环遍历键名创建一个类型;

ts
interface IPerson {
    name: string
    age:number
}

type MapType<Type> = {
+    [property in keyof Type]: Type[property]
}

type NewPerson = MapType<IPerson>
  • property:属性名变量,名字可以随便起。
  • in:运算符,用来取出右侧的联合类型的每一个成员。
  • keyof Type:返回类型Type的每一个属性名,组成一个联合类型。如"name" | "age"

注意: 映射类型不能使用 interface 定义

映射修饰符

在使用映射类型时,有两个额外的修饰符可能会用到:

  • readonly:用于设置属性只读;

  • ? :用于设置属性可选;

你可以通过前缀 - 或者 + 删除或者添加这些修饰符,如果没有写前缀,相当于使用了 + 前缀。

▸ 所有属性删除可选属性

image-20240607111022925

▸ 所有属性变成可选只读属性

image-20230601114538149

TS-条件类型

条件类型

很多时候,日常开发中我们需要基于输入的值来决定输出的值,同样我们也需要基于输入的值的类型来决定输出的值的类型。

条件类型(Conditional types)就是用来帮助我们描述输入类型和输出类型之间的关系

  • 条件类型的写法有点类似于 JavaScript 中的条件表达式(condition ? trueExpression : falseExpression ):

    ts
    SomeType extends OtherType ? TrueType : FalseType;

示例: 基本示例

image-20230601181112545

示例: 函数重载

image-20230601181607354

在条件类型中推断 infer

在条件类型中推断(Inferring Within Conditional Types)

  • 条件类型提供了 infer 关键字,可以从正在比较的类型中推断类型,然后在 true 分支里引用该推断结果

比如我们现在有一个函数类型,想要获取到一个函数的参数类型和返回值类型:

image-20240612115842474

分发条件类型

当在泛型中使用条件类型的时候,如果传入一个联合类型,就会变成 分发的(distributive)

image-20230517151613039

如果我们在 ToArray 传入一个联合类型,这个条件类型会被应用到联合类型的每个成员:

  • 当传入string | number时,会遍历联合类型中的每一个成员

  • 相当于ToArray<string> | ToArray<number>

  • 所以最后的结果是:string[] | number[]

类型工具和类型体操

内置工具和类型体操

类型系统其实在很多语言里面都是有的,比如 Java、Swift、C++等等,但是相对来说 TypeScript 的类型非常灵活:

  • 这是因为 TypeScript 的目的是为 JavaScript 添加一套类型校验系统,因为 JavaScript 本身的灵活性,也让 TypeScript 类型系统不得不增加更加复杂的功能以适配 JavaScript 的灵活性;

  • 所以TypeScript是一种可以支持类型编程的类型系统

这种类型编程系统为 TypeScript 增加了很大的灵活度,同时也增加了它的难度:

  • 如果你仅仅在开发业务的时候为自己的 JavaScript 代码增加上类型约束,那么基本不需要太多的类型编程能力;

  • 但是如果你在开发一些框架、库,或者通用性的工具,为了考虑各种适配的情况,就需要使用类型编程;

TypeScript 本身为我们提供了类型工具,帮助我们辅助进行类型转换(前面有用过关于 this 的类型工具)。

很多开发者为了进一步增强自己的 TypeScript 编程能力,还会专门去做一些类型体操的题目:

我们课堂上会学习 TypeScript 的编程能力的语法,并且通过学习内置工具来练习一些类型体操的题目。

Partial<Type>

用于构造一个 Type 下面的所有属性都设置为可选的类型

image-20230517151720849

Required<Type>

用于构造一个 Type 下面的所有属性全都设置为必填的类型,这个工具类型跟 Partial 相反。

image-20230517151745676

Readonly<Type>

用于构造一个 Type 下面的所有属性全都设置为只读的类型,意味着这个类型的所有的属性全都不可以重新赋值。

image-20230517151757959

Record<Keys, Type>

用于构造一个对象类型,它所有的 key(键)都是 Keys 类型,它所有的 value(值)都是 Type 类型。

注意: Keys 必须是一个联合类型,因为索引签名的 in 后面只能是一个联合类型[P in Keys],为了确保 Keys 是一个联合类型就需要给它一个约束Keys extends keyof any

image-20230517151820892

Pick<Type, Keys>

用于构造一个类型,它是从 Type 类型里面挑了一些属性 Keys

image-20230517151831180

Omit<Type, Keys>

用于构造一个类型,它是从 Type 类型里面过滤了一些属性 Keys

image-20240612151702188

Exclude<UnionType, ExcludedMembers>

用于构造一个类型,它是从 UnionType 联合类型里面排除了所有可以赋给 ExcludedMembers 的类型。

此处利用了条件分发

image-20230517151933608

有了 HYExclude,我们可以使用它来实现 HYOmit。

Extract<Type, Union>

用于构造一个类型,它是从 Type 类型里面提取了所有可以赋给 Union 的类型。

image-20230517151951299

NonNullable<Type>

用于构造一个类型,这个类型从 Type 中排除了所有的 null、undefined 的类型。

image-20230517152001251

ReturnType<Type>

用于构造一个含有 Type 函数的返回值的类型。

image-20230517152014583

InstanceType<typeof Type>

用于构造一个由所有 Type 的构造函数的实例类型组成的类型。

解析:

  • typeof Person : 获取 Person 构造函数的类型
  • InstanceType<typeof Person>:获取构造函数创建的实例对象的类型

image-20230517152029426

语法扩展

TS 模块化

TS 模块化

JavaScript 有一个很长的处理模块化代码的历史,TypeScript 从 2012 年开始跟进,现在已经实现支持了很多格式。但是随着

时间流逝,社区和 JavaScript 规范已经使用为名为 ES Module 的格式,这也就是我们所知的 import/export 语法。

  • ES 模块在 2015 年被添加到 JavaScript 规范中,到 2020 年,大部分的 web 浏览器和 JavaScript 运行环境都已经广泛支持。

  • 所以在TypeScript中最主要使用的模块化方案就是ES Module

image-20230601122632595

在前面我们已经学习过各种各样模块化方案以及对应的细节,这里我们主要学习 TypeScript 中一些比较特别的细节。

非模块

我们需要先理解 TypeScript 认为什么是一个模块。

  • JavaScript 规范声明任何没有 export 的 JavaScript 文件都应该被认为是一个脚本,而非一个模块

  • 在一个脚本文件中,变量和类型会被声明在共享的全局作用域,将多个输入文件合并成一个输出文件,或者在 HTML 使用多个 <script> 标签加载这些文件。

如果你有一个文件,现在没有任何 import 或者 export,但是你希望它被作为模块处理,添加这行代码:

image-20230601122640484

这会把文件改成一个没有导出任何内容的模块,这个语法可以生效,无论你的模块目标是什么。

内置类型导入

TypeScript 4.5 也允许单独导入类型,你需要使用 type 前缀 ,表明被导入的是一个类型:

image-20230601122700853

image-20230601130802718

导入时添加 type 前缀可以让一个非 TypeScript 编译器比如 Babel、swc 或者 esbuild 知道什么样的导入可以被安全移除。**

image-20230601122707853

TS 命名空间

命名空间 namespace(了解)

TypeScript 有它自己的模块格式,名为 namespace ,它在 ES 模块标准之前出现。

  • 命名空间在 TypeScript 早期时,称之为内部模块目的是将一个模块内部再进行作用域的划分,防止一些命名冲突的问题;

  • 虽然命名空间没有被废弃,但是由于 ES 模块已经拥有了命名空间的大部分特性,因此更推荐使用 ES 模块,这样才能与 JavaScript 的(发展)方向保持一致。

image-20230601122721690

注意: 如果要在外部使用 namespace 中的函数、变量也需要将它们 export 导出,才能使用

使用命名空间

ts
import { Time, Price } from 'xxx'
Time.format('2023-02-02')
Price.format('110')

webpack 搭建 TS 运行环境

1、初始化 package.json

sh
# npm
npm init -y

#pnpm
pnpm init

2、安装 webpack

sh
npm i webpack -D
npm i webpack-cli -D

3、安装:ts-loader

sh
npm i ts-loader -D

4、安装:html-webpack-plugin

sh
npm i html-webpack-plugin -D

5、安装:webpack-dev-server

sh
npm i webpack-dev-server -D

在 package.json 中设置:

json
"scripts": {
    "server": "webpack serve",
    "build": "webpack"
}

6、初始化 tsconfig.json

sh
tsc --init

7、webpack 配置文件

  • 创建webpack.config.cjs 文件

  • 配置文件

    js
    const path = require('path')
    cosnst HtmlWebpackPlugin = require('html-webpack-plugin')
    
    module.exports = {
        mode: "development",
        entry: "./src/index.ts",
        output: {
            path: path.resolve(__dirname, './dist'),
            filename: 'bundle.js'
        },
        resolve: {
            extensions: ['.ts', '.js', '.cjs', '.json']
        },
        module: {
            rules: [
                {
                    test: /\.ts$/,
                    loader: 'ts-loader'
                }
            ]
        },
        plugins: [
            new HtmlWebpackPlugin({
                template: './index.html'
            })
        ],
        devServer:{}
    }

8、运行项目

sh
npm run serve

补充: webpack.config 代码提示

问题: 在编写 webpack 的配置文件 webpack.config.js 时,VSCode 没有代码智能提示,没法自动补全

解决:webpack.config.js 文件里添加如下配置:

js
/**
 * @type {import('webpack').Configuration}
 */

效果:

声明文件-内置

类型的查找

.d.ts文件:用来做类型的声明(declare),称之为类型声明(Type Declaration)或者类型定义(Type Definition)文件。

之前我们所有的 typescript 中的类型,几乎都是我们自己编写的,但是我们也有用到一些其他的类型:

image-20230601122735771

大家是否会奇怪,我们的 HTMLImageElement 类型来自哪里呢?甚至是 document 为什么可以有 getElementById 的方法呢?

  • 其实这里就涉及到 typescript 对类型的管理和查找规则了。

我们这里先给大家介绍另外的一种 typescript 文件:.d.ts文件

  • 我们之前编写的 typescript 文件都是 .ts 文件,这些文件最终会输出 .js 文件,也是我们通常编写代码的地方;

  • 还有另外一种文件 .d.ts 文件,它是用来做类型的声明(declare),称之为类型声明(Type Declaration)或者类型定义(Type Definition)文件。

  • 它仅仅用来做类型检测,告知 typescript 我们有哪些类型;

那么 typescript 会在哪里查找我们的类型声明呢?

  • 内置类型声明;

  • 外部定义类型声明;

  • 自己定义类型声明;

内置类型声明

内置类型声明是 typescript 自带的、帮助我们内置了 JavaScript 运行时的一些标准化 API 的声明文件;

  • 包括比如 Function、String、Math、Date 等内置类型;

  • 也包括运行环境中的 DOM API,比如 Window、Document 等;

TypeScript 使用模式命名这些声明文件lib.[something].d.ts

image-20230601122759345

内置类型声明通常在我们安装 typescript 的环境中会带有的;

内置声明的环境

我们可以通过targetlib来决定哪些内置类型声明是可以使用的:

  • 例如,startsWith 字符串方法只能从称为 ECMAScript 6 的 JavaScript 版本开始使用;

我们可以通过 target 的编译选项来配置:TypeScript 通过 lib 根据您的 target 设置更改默认包含的文件来帮助解决此问题。

image-20230601141204865

image-20230601122826853

声明文件-第三方库

外部定义类型声明-第三方库

外部类型声明通常是我们使用一些库(比如第三方库)时,需要的一些类型声明。

这些库通常有两种类型声明方式:

方式一:在自己库中进行类型声明(编写.d.ts 文件),比如 axios

方式二:通过社区的一个公有库 DefinitelyTyped 存放类型声明文件

image-20230601122901054

外部定义类型声明-自定义声明

什么情况下需要自己来定义声明文件呢?

  • 情况一:我们使用的第三方库是一个纯的 JavaScript 库,没有对应的声明文件;比如 lodash

  • 情况二:我们给自己的代码中声明一些类型,方便在其他地方直接进行使用;

image-20230601122937495

实际应用:

  • 平常使用的代码中用到的类型,可以直接在当前位置进行定义,或在业务文件夹的某个位置编写一个类型文件即可
  • 在其他模块中的全局变量/函数/类,如果想使用它们,需要先对其进行类型声明之后 TS 才允许使用

声明文件-自定义

declare 声明模块

我们也可以声明模块,比如 lodash 模块默认不能使用的情况,可以自己来声明这个模块:

image-20230601122950985

语法:

ts
declare module '模块名' {
    export function ...
    export class ...
    export const ...
}

在声明模块的内部,我们可以通过 export 导出对应库的类、函数等;

declare 声明文件

在某些情况下,我们也可以声明文件:

  • 比如在开发 vue 的过程中,默认是不识别我们的.vue 文件的,那么我们就需要对其进行文件的声明;

  • 比如在开发中我们使用了 jpg 这类图片文件,默认 typescript 也是不支持的,也需要对其进行声明;

image-20230601123002350

image-20230601150222624

declare 命名空间

比如我们在 index.html 中直接引入了 jQuery:

我们可以进行命名空间的声明:

image-20230601123011749

在 main.ts 中就可以使用了:

image-20230601123019390

配置文件-tsconfig.json

认识 tsconfig.json 文件

什么是 tsconfig.json 文件呢?(官方的解释)

  • 当目录中出现了 tsconfig.json 文件,则说明该目录是 TypeScript 项目的根目录;

  • tsconfig.json 文件指定了编译项目所需的根目录下的文件以及编译选项

官方的解释有点“官方”,直接看我的解释。

tsconfig.json 文件有两个作用

  • 作用一(主要的作用):让TypeScript Compiler(tsc)在编译的时候,知道如何去编译 TypeScript 代码和进行类型检测

    • 比如是否允许不明确的 this 选项,是否允许隐式的 any 类型;
    • 将 TypeScript 代码编译成什么版本的 JavaScript 代码;
  • 作用二:让编辑器(比如 VSCode)可以按照正确的方式识别 TypeScript 代码

    • 对于哪些语法进行提示、类型错误检测等等;

JavaScript 项目可以使用 jsconfig.json 文件,它的作用与 tsconfig.json 基本相同,只是默认启用了一些 JavaScript 相关的编译选项。

  • 在之前的 Vue 项目、React 项目中我们也有使用过;

tsconfig.json 配置

tsconfig.json 在编译时如何被使用呢?

  • 在调用 tsc 命令并且没有其它输入文件参数时,编译器将由当前目录开始向父级目录寻找包含 tsconfig 文件的目录。如: tsc

  • 调用 tsc 命令并且没有其他输入文件参数,可以使用 --project (或者只是 -p)的命令行选项来指定包含了 tsconfig.json 的目录,如:tsc -p 目录

  • 当命令行中指定了输入文件参数, tsconfig.json 文件会被忽略,如:tsc index.ts

webpack 中使用 ts-loader 进行打包时,也会自动读取 tsconfig 文件,根据配置编译 TypeScript 代码。

tsconfig.json 文件包括哪些选项呢?

  • tsconfig.json 本身包括的选项非常非常多,我们不需要每一个都记住;

  • 可以查看文档对于每个选项的解释:https://www.typescriptlang.org/tsconfig

  • 当我们开发项目的时候,选择 TypeScript 模板时,tsconfig 文件默认都会帮助我们配置好的;

接下来我们学习一下哪些重要的、常见的选项。

tsconfig.json 顶层选项

  • include[]默认:全部,指定哪些文件需要被编译。
  • exclude[]默认:node_module,指定从 include 中排除哪些文件 。
  • files[],指定被编译的文件列表,只有编译少量文件才使用
  • extendsstring,指定继承的配置文件
  • compilerOptions{},编译器的选项
    • target:``,
    • lib:``,
    • module:``,
    • :``,
    • :``,

注意:

  • **:任意目录
  • * :任意文件

image-20230601123146511

tsconfig.json 常见配置

tsconfig.json 是用于配置 TypeScript 编译时的配置选项:

我们这里讲解几个比较常见的:

image-20230601123200230

image-20230601123209997

Axios 封装@

Axios-基本封装

1、HYRequest 类

ts
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig } from 'axios'

class MrRequest {
  instance: AxiosInstance

  constructor(config: AxiosRequestConfig) {
+    this.instance = axios.create(config)
  }

  request(config: AxiosRequestConfig) {
+    return this.instance.request(config)
  }

  get() {}

  post() {}
}

export default MrRequest

2、配置

ts
export const BASE_URL = 'http://codercba.com:8000'
export const TIME_OUT = 10000

3、实例化 HYRequest

ts
import MrRequest from './request'
import { BASE_URL, TIME_OUT } from './config'

const mrRequest = new MrRequest({
  baseURL: BASE_URL,
  timeout: TIME_OUT
})

export default mrRequest

4、使用 Request 发送网络请求

ts
import mrRequest from '@/utils/request'

mrRequest
  .request({
    url: '/home/multidata'
  })
  .then((res) => {
    console.log(res)
  })

Axios-拦截器

image-20241031173853033

1、全局拦截器

ts
class MrRequest {
  instance: AxiosInstance

  constructor(config: MrRequestConfig) {
    this.instance = axios.create(config)

    // 1. 全局拦截器
    this.instance.interceptors.request.use(
      (config) => {
        console.log('全局-请求-拦截成功')
        return config
      },
      (err) => {
        console.log('全局-请求-拦截失败')
        return err
      }
    )
    this.instance.interceptors.response.use(
      (res) => {
        console.log('全局-响应-拦截成功')
        return res
      },
      (err) => {
        console.log('全局-响应-拦截失败')
        return err
      }
    )
  }
}

2、实例拦截器

1、声明 MrRequestConfig 类型

ts
interface MrInterceptors {
  requestSuccessFn: (config: AxiosRequestConfig) => AxiosRequestConfig
  requestFailureFn: (err: any) => any
  responseSuccessFn: (res: AxiosResponse) => AxiosResponse
  responseFalureFn: (err: any) => any
}

export interface MrRequestConfig extends AxiosRequestConfig {
  interceptors?: MrInterceptors
}

2、针对特定的 mrRequest 实例添加拦截器

ts
  constructor(config: MrRequestConfig) {
    this.instance = axios.create(config)
    // 1. 全局拦截器
      ...
    // 2. 实例拦截器
    if (config.interceptors) {
      this.instance.interceptors.request.use(
        config.interceptors.requestSuccessFn,
        config.interceptors.requestFailureFn
      )
      this.instance.interceptors.response.use(
        config.interceptors.responseSuccessFn,
        config.interceptors.responseFalureFn
      )
    }
  }

3、在实例化时单独添加拦截器

ts
const mrRequest = new MrRequest({
  baseURL: BASE_URL,
  timeout: TIME_OUT,

  interceptors: {
    requestSuccessFn: (config) => {
      console.log('实例-请求-拦截成功')
      return config
    },
    requestFailureFn: (err) => {
      console.log('实例-请求-拦截失败')
      return err
    },
    responseSuccessFn: (res) => {
      console.log('实例-响应-拦截成功')
      return res
    },
    responseFalureFn: (err) => {
      console.log('实例-响应-拦截失败')
      return err
    }
  }
})

3、单次请求拦截器

ts
class MrRequest {
  request(config: MrRequestConfig) {
    // 3. 请求拦截器
    if (config.interceptors?.requestSuccessFn) {
      config.interceptors.requestSuccessFn(config)
    }

    return new Promise((resolve, reject) => {
      this.instance.request(config).then(
        (res) => {
          if (config.interceptors?.responseSuccessFn) {
            res = config.interceptors.responseSuccessFn(res)
          }
          resolve(res)
        },
        (err) => {
          reject(err)
        }
      )
    })
  }
}

修改拦截器函数为可选

ts
interface MrInterceptors {
  requestSuccessFn?: (config: AxiosRequestConfig) => AxiosRequestConfig
  requestFailureFn?: (err: any) => any
  responseSuccessFn?: (res: AxiosResponse) => AxiosResponse
  responseFalureFn?: (err: any) => any
}
interface MrRequestConfig extends AxiosRequestConfig {
  interceptors?: MrInterceptors
}

在单次请求中添加单独的拦截器

ts
import mrRequest from '@/utils/request'

mrRequest
  .request({
    url: '/home/multidata'

    interceptors: {
      requestSuccessFn: (config) => {
        console.log('单次-请求-拦截成功')
        return config
      },
      responseSuccessFn: (res) => {
        console.log('单次-响应-拦截成功')
        return res
      }
    }
  })
  .then((res) => {
    console.log(res)
  })

Axios-返回结果的类型处理

问题 1: request 的返回结果 res 为unknown 类型

image-20230601172914889

原因:new Promise 时,需要传入一个类型,否则就是 unknown 类型

image-20230601173029164

解决: 在 new Promise 时传入具体的类型:AxiosResponse

image-20230601173232507

问题 2: 在实际项目中,真正有用的数据是 res.data,所以一般会在响应拦截中返回res.data ,此时它的数据类型就不是AxiosResponse 了。此时偷懒的办法是设为 any 类型,讲究的办法就是对 data 数据定义类型

image-20241031181230270

image-20230601174321467

更优雅的类型转化

image-20230601174624890

此时 res 就是 IHomeData 类型了

image-20230601173952658

Axios-接口类型中的泛型处理

问题

image-20230601175315371

解决:在定义接口时给 T 一个默认值

image-20230601175455228

在使用到 HYRequestConfig 时传入单独的类型

image-20230601175705417

Axios-其他方法

ts
  get<T = any>(config: MrRequestConfig<T>) {
    return this.request({ ...config, method: 'GET' })
  }

  post<T = any>(config: MrRequestConfig<T>) {
    return this.request({ ...config, method: 'POST' })
  }

  delete<T = any>(config: MrRequestConfig<T>) {
    return this.request({ ...config, method: 'DELETE' })
  }

  patch<T = any>(config: MrRequestConfig<T>) {
    return this.request({ ...config, method: 'PATCH' })
  }
}